/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validation_test

import (
	"strings"
	"testing"
	"time"

	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	utilfeature "k8s.io/apiserver/pkg/util/feature"
	logsapi "k8s.io/component-base/logs/api/v1"
	kubeletconfig "k8s.io/kubernetes/pkg/kubelet/apis/config"
	"k8s.io/kubernetes/pkg/kubelet/apis/config/validation"
	kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
	utilpointer "k8s.io/utils/pointer"
)

var (
	successConfig = kubeletconfig.KubeletConfiguration{
		CgroupsPerQOS:                   true,
		EnforceNodeAllocatable:          []string{"pods", "system-reserved", "kube-reserved"},
		SystemReservedCgroup:            "/system.slice",
		KubeReservedCgroup:              "/kubelet.service",
		SystemCgroups:                   "",
		CgroupRoot:                      "",
		EventBurst:                      10,
		EventRecordQPS:                  5,
		HealthzPort:                     10248,
		ImageGCHighThresholdPercent:     85,
		ImageGCLowThresholdPercent:      80,
		IPTablesDropBit:                 15,
		IPTablesMasqueradeBit:           14,
		KubeAPIBurst:                    10,
		KubeAPIQPS:                      5,
		MaxOpenFiles:                    1000000,
		MaxPods:                         110,
		OOMScoreAdj:                     -999,
		PodsPerCore:                     100,
		Port:                            65535,
		ReadOnlyPort:                    0,
		RegistryBurst:                   10,
		RegistryPullQPS:                 5,
		HairpinMode:                     kubeletconfig.PromiscuousBridge,
		NodeLeaseDurationSeconds:        1,
		CPUCFSQuotaPeriod:               metav1.Duration{Duration: 25 * time.Millisecond},
		TopologyManagerScope:            kubeletconfig.PodTopologyManagerScope,
		TopologyManagerPolicy:           kubeletconfig.SingleNumaNodeTopologyManagerPolicy,
		ShutdownGracePeriod:             metav1.Duration{Duration: 30 * time.Second},
		ShutdownGracePeriodCriticalPods: metav1.Duration{Duration: 10 * time.Second},
		MemoryThrottlingFactor:          utilpointer.Float64Ptr(0.8),
		FeatureGates: map[string]bool{
			"CustomCPUCFSQuotaPeriod": true,
			"GracefulNodeShutdown":    true,
			"MemoryQoS":               true,
		},
		Logging: logsapi.LoggingConfiguration{
			Format: "text",
		},
	}
)

func TestValidateKubeletConfiguration(t *testing.T) {
	featureGate := utilfeature.DefaultFeatureGate.DeepCopy()
	logsapi.AddFeatureGates(featureGate)

	cases := []struct {
		name      string
		configure func(config *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration
		errMsg    string
	}{
		{
			name: "Success",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				return conf
			},
		},
		{
			name: "invalid NodeLeaseDurationSeconds",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.NodeLeaseDurationSeconds = 0
				return conf
			},
			errMsg: "invalid configuration: nodeLeaseDurationSeconds must be greater than 0",
		},
		{
			name: "specify EnforceNodeAllocatable without enabling CgroupsPerQOS",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.CgroupsPerQOS = false
				conf.EnforceNodeAllocatable = []string{"pods"}
				return conf
			},
			errMsg: "invalid configuration: enforceNodeAllocatable (--enforce-node-allocatable) is not supported unless cgroupsPerQOS (--cgroups-per-qos) is set to true",
		},
		{
			name: "specify SystemCgroups without CgroupRoot",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.SystemCgroups = "/"
				conf.CgroupRoot = ""
				return conf
			},
			errMsg: "invalid configuration: systemCgroups (--system-cgroups) was specified and cgroupRoot (--cgroup-root) was not specified",
		},
		{
			name: "invalid EventBurst",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EventBurst = -1
				return conf
			},
			errMsg: "invalid configuration: eventBurst (--event-burst) -1 must not be a negative number",
		},
		{
			name: "invalid EventRecordQPS",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EventRecordQPS = -1
				return conf
			},
			errMsg: "invalid configuration: eventRecordQPS (--event-qps) -1 must not be a negative number",
		},
		{
			name: "invalid HealthzPort",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.HealthzPort = 65536
				return conf
			},
			errMsg: "invalid configuration: healthzPort (--healthz-port) 65536 must be between 1 and 65535, inclusive",
		},
		{
			name: "specify CPUCFSQuotaPeriod without enabling CPUCFSQuotaPeriod",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"CustomCPUCFSQuotaPeriod": false}
				conf.CPUCFSQuotaPeriod = metav1.Duration{Duration: 200 * time.Millisecond}
				return conf
			},
			errMsg: "invalid configuration: cpuCFSQuotaPeriod (--cpu-cfs-quota-period) {200ms} requires feature gate CustomCPUCFSQuotaPeriod",
		},
		{
			name: "invalid CPUCFSQuotaPeriod",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"CustomCPUCFSQuotaPeriod": true}
				conf.CPUCFSQuotaPeriod = metav1.Duration{Duration: 2 * time.Second}
				return conf
			},
			errMsg: "invalid configuration: cpuCFSQuotaPeriod (--cpu-cfs-quota-period) {2s} must be between 1usec and 1sec, inclusive",
		},
		{
			name: "invalid ImageGCHighThresholdPercent",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ImageGCHighThresholdPercent = 101
				return conf
			},
			errMsg: "invalid configuration: imageGCHighThresholdPercent (--image-gc-high-threshold) 101 must be between 0 and 100, inclusive",
		},
		{
			name: "invalid ImageGCLowThresholdPercent",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ImageGCLowThresholdPercent = -1
				return conf
			},
			errMsg: "invalid configuration: imageGCLowThresholdPercent (--image-gc-low-threshold) -1 must be between 0 and 100, inclusive",
		},
		{
			name: "ImageGCLowThresholdPercent is equal to ImageGCHighThresholdPercent",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ImageGCHighThresholdPercent = 0
				conf.ImageGCLowThresholdPercent = 0
				return conf
			},
			errMsg: "invalid configuration: imageGCLowThresholdPercent (--image-gc-low-threshold) 0 must be less than imageGCHighThresholdPercent (--image-gc-high-threshold) 0",
		},
		{
			name: "ImageGCLowThresholdPercent is greater than ImageGCHighThresholdPercent",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ImageGCHighThresholdPercent = 0
				conf.ImageGCLowThresholdPercent = 1
				return conf
			},
			errMsg: "invalid configuration: imageGCLowThresholdPercent (--image-gc-low-threshold) 1 must be less than imageGCHighThresholdPercent (--image-gc-high-threshold) 0",
		},
		{
			name: "invalid IPTablesDropBit",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.IPTablesDropBit = 32
				return conf
			},
			errMsg: "invalid configuration: iptablesDropBit (--iptables-drop-bit) 32 must be between 0 and 31, inclusive",
		},
		{
			name: "invalid IPTablesMasqueradeBit",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.IPTablesMasqueradeBit = 32
				return conf
			},
			errMsg: "invalid configuration: iptablesMasqueradeBit (--iptables-masquerade-bit) 32 must be between 0 and 31, inclusive",
		},
		{
			name: "invalid KubeAPIBurst",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.KubeAPIBurst = -1
				return conf
			},
			errMsg: "invalid configuration: kubeAPIBurst (--kube-api-burst) -1 must not be a negative number",
		},
		{
			name: "invalid KubeAPIQPS",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.KubeAPIQPS = -1
				return conf
			},
			errMsg: "invalid configuration: kubeAPIQPS (--kube-api-qps) -1 must not be a negative number",
		},
		{
			name: "invalid NodeStatusMaxImages",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.NodeStatusMaxImages = -2
				return conf
			},
			errMsg: "invalid configuration: nodeStatusMaxImages (--node-status-max-images) -2 must be -1 or greater",
		},
		{
			name: "invalid MaxOpenFiles",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.MaxOpenFiles = -1
				return conf
			},
			errMsg: "invalid configuration: maxOpenFiles (--max-open-files) -1 must not be a negative number",
		},
		{
			name: "invalid MaxPods",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.MaxPods = -1
				return conf
			},
			errMsg: "invalid configuration: maxPods (--max-pods) -1 must not be a negative number",
		},
		{
			name: "invalid OOMScoreAdj",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.OOMScoreAdj = 1001
				return conf
			},
			errMsg: "invalid configuration: oomScoreAdj (--oom-score-adj) 1001 must be between -1000 and 1000, inclusive",
		},
		{
			name: "invalid PodsPerCore",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.PodsPerCore = -1
				return conf
			},
			errMsg: "invalid configuration: podsPerCore (--pods-per-core) -1 must not be a negative number",
		},
		{
			name: "invalid Port",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.Port = 65536
				return conf
			},
			errMsg: "invalid configuration: port (--port) 65536 must be between 1 and 65535, inclusive",
		},
		{
			name: "invalid ReadOnlyPort",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ReadOnlyPort = 65536
				return conf
			},
			errMsg: "invalid configuration: readOnlyPort (--read-only-port) 65536 must be between 0 and 65535, inclusive",
		},
		{
			name: "invalid RegistryBurst",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.RegistryBurst = -1
				return conf
			},
			errMsg: "invalid configuration: registryBurst (--registry-burst) -1 must not be a negative number",
		},
		{
			name: "invalid RegistryPullQPS",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.RegistryPullQPS = -1
				return conf
			},
			errMsg: "invalid configuration: registryPullQPS (--registry-qps) -1 must not be a negative number",
		},
		{
			name: "specify ServerTLSBootstrap without enabling RotateKubeletServerCertificate",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"RotateKubeletServerCertificate": false}
				conf.ServerTLSBootstrap = true
				return conf
			},
			errMsg: "invalid configuration: serverTLSBootstrap true requires feature gate RotateKubeletServerCertificate",
		},
		{
			name: "use SingleNumaNodeTopologyManagerPolicy without enabling TopologyManager",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"TopologyManager": false}
				conf.TopologyManagerPolicy = kubeletconfig.SingleNumaNodeTopologyManagerPolicy
				return conf
			},
			errMsg: "invalid configuration: topologyManagerPolicy single-numa-node requires feature gate TopologyManager",
		},
		{
			name: "invalid TopologyManagerPolicy",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.TopologyManagerPolicy = "invalid-policy"
				return conf
			},
			errMsg: "invalid configuration: topologyManagerPolicy (--topology-manager-policy) \"invalid-policy\" must be one of: [\"none\" \"best-effort\" \"restricted\" \"single-numa-node\"]",
		},
		{
			name: "use PodTopologyManagerScope without enabling TopologyManager",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"TopologyManager": false}
				conf.TopologyManagerScope = kubeletconfig.PodTopologyManagerScope
				return conf
			},
			errMsg: "invalid configuration: topologyManagerScope pod requires feature gate TopologyManager",
		},
		{
			name: "invalid TopologyManagerScope",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.TopologyManagerScope = "invalid-scope"
				return conf
			},
			errMsg: "invalid configuration: topologyManagerScope (--topology-manager-scope) \"invalid-scope\" must be one of: \"container\", or \"pod\"",
		},
		{
			name: "ShutdownGracePeriodCriticalPods is greater than ShutdownGracePeriod",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"GracefulNodeShutdown": true}
				conf.ShutdownGracePeriodCriticalPods = metav1.Duration{Duration: 2 * time.Second}
				conf.ShutdownGracePeriod = metav1.Duration{Duration: 1 * time.Second}
				return conf
			},
			errMsg: "invalid configuration: shutdownGracePeriodCriticalPods {2s} must be <= shutdownGracePeriod {1s}",
		},
		{
			name: "ShutdownGracePeriod is less than 1 sec",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"GracefulNodeShutdown": true}
				conf.ShutdownGracePeriod = metav1.Duration{Duration: 1 * time.Millisecond}
				return conf
			},
			errMsg: "invalid configuration: shutdownGracePeriod {1ms} must be either zero or otherwise >= 1 sec",
		},
		{
			name: "ShutdownGracePeriodCriticalPods is less than 1 sec",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"GracefulNodeShutdown": true}
				conf.ShutdownGracePeriodCriticalPods = metav1.Duration{Duration: 1 * time.Millisecond}
				return conf
			},
			errMsg: "invalid configuration: shutdownGracePeriodCriticalPods {1ms} must be either zero or otherwise >= 1 sec",
		},
		{
			name: "specify ShutdownGracePeriod without enabling GracefulNodeShutdown",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"GracefulNodeShutdown": false}
				conf.ShutdownGracePeriod = metav1.Duration{Duration: 1 * time.Second}
				return conf
			},
			errMsg: "invalid configuration: specifying shutdownGracePeriod or shutdownGracePeriodCriticalPods requires feature gate GracefulNodeShutdown",
		},
		{
			name: "specify ShutdownGracePeriodCriticalPods without enabling GracefulNodeShutdown",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"GracefulNodeShutdown": false}
				conf.ShutdownGracePeriodCriticalPods = metav1.Duration{Duration: 1 * time.Second}
				return conf
			},
			errMsg: "invalid configuration: specifying shutdownGracePeriod or shutdownGracePeriodCriticalPods requires feature gate GracefulNodeShutdown",
		},
		{
			name: "invalid MemorySwap.SwapBehavior",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"NodeSwap": true}
				conf.MemorySwap.SwapBehavior = "invalid-behavior"
				return conf
			},
			errMsg: "invalid configuration: memorySwap.swapBehavior \"invalid-behavior\" must be one of: \"\", \"LimitedSwap\", or \"UnlimitedSwap\"",
		},
		{
			name: "specify MemorySwap.SwapBehavior without enabling NodeSwap",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"NodeSwap": false}
				conf.MemorySwap.SwapBehavior = kubetypes.LimitedSwap
				return conf
			},
			errMsg: "invalid configuration: memorySwap.swapBehavior cannot be set when NodeSwap feature flag is disabled",
		},
		{
			name: "specify SystemReservedEnforcementKey without specifying SystemReservedCgroup",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EnforceNodeAllocatable = []string{kubetypes.SystemReservedEnforcementKey}
				conf.SystemReservedCgroup = ""
				return conf
			},
			errMsg: "invalid configuration: systemReservedCgroup (--system-reserved-cgroup) must be specified when \"system-reserved\" contained in enforceNodeAllocatable (--enforce-node-allocatable)",
		},
		{
			name: "specify KubeReservedEnforcementKey without specifying KubeReservedCgroup",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EnforceNodeAllocatable = []string{kubetypes.KubeReservedEnforcementKey}
				conf.KubeReservedCgroup = ""
				return conf
			},
			errMsg: "invalid configuration: kubeReservedCgroup (--kube-reserved-cgroup) must be specified when \"kube-reserved\" contained in enforceNodeAllocatable (--enforce-node-allocatable)",
		},
		{
			name: "specify NodeAllocatableNoneKey with additional enforcements",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EnforceNodeAllocatable = []string{kubetypes.NodeAllocatableNoneKey, kubetypes.KubeReservedEnforcementKey}
				return conf
			},
			errMsg: "invalid configuration: enforceNodeAllocatable (--enforce-node-allocatable) may not contain additional enforcements when \"none\" is specified",
		},
		{
			name: "invalid EnforceNodeAllocatable",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.EnforceNodeAllocatable = []string{"invalid-enforce-node-allocatable"}
				return conf
			},
			errMsg: "invalid configuration: option \"invalid-enforce-node-allocatable\" specified for enforceNodeAllocatable (--enforce-node-allocatable). Valid options are \"pods\", \"system-reserved\", \"kube-reserved\", or \"none\"",
		},
		{
			name: "invalid HairpinMode",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.HairpinMode = "invalid-hair-pin-mode"
				return conf
			},
			errMsg: "invalid configuration: option \"invalid-hair-pin-mode\" specified for hairpinMode (--hairpin-mode). Valid options are \"none\", \"hairpin-veth\" or \"promiscuous-bridge\"",
		},
		{
			name: "specify ReservedSystemCPUs with SystemReservedCgroup",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ReservedSystemCPUs = "0-3"
				conf.SystemReservedCgroup = "/system.slice"
				return conf
			},
			errMsg: "invalid configuration: can't use reservedSystemCPUs (--reserved-cpus) with systemReservedCgroup (--system-reserved-cgroup) or kubeReservedCgroup (--kube-reserved-cgroup)",
		},
		{
			name: "specify ReservedSystemCPUs with KubeReservedCgroup",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ReservedSystemCPUs = "0-3"
				conf.KubeReservedCgroup = "/system.slice"
				return conf
			},
			errMsg: "invalid configuration: can't use reservedSystemCPUs (--reserved-cpus) with systemReservedCgroup (--system-reserved-cgroup) or kubeReservedCgroup (--kube-reserved-cgroup)",
		},
		{
			name: "invalid ReservedSystemCPUs",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.ReservedSystemCPUs = "invalid-reserved-system-cpus"
				return conf
			},
			errMsg: "invalid configuration: unable to parse reservedSystemCPUs (--reserved-cpus) invalid-reserved-system-cpus, error:",
		},
		{
			name: "enable MemoryQoS without specifying MemoryThrottlingFactor",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.FeatureGates = map[string]bool{"MemoryQoS": true}
				conf.MemoryThrottlingFactor = nil
				return conf
			},
			errMsg: "invalid configuration: memoryThrottlingFactor is required when MemoryQoS feature flag is enabled",
		},
		{
			name: "invalid MemoryThrottlingFactor",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				conf.MemoryThrottlingFactor = utilpointer.Float64(1.1)
				return conf
			},
			errMsg: "invalid configuration: memoryThrottlingFactor 1.1 must be greater than 0 and less than or equal to 1.0",
		},
		{
			name: "invalid Taint.TimeAdded",
			configure: func(conf *kubeletconfig.KubeletConfiguration) *kubeletconfig.KubeletConfiguration {
				now := metav1.Now()
				conf.RegisterWithTaints = []v1.Taint{{TimeAdded: &now}}
				return conf
			},
			errMsg: "invalid configuration: taint.TimeAdded is not nil",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			errs := validation.ValidateKubeletConfiguration(tc.configure(successConfig.DeepCopy()), featureGate)

			if len(tc.errMsg) == 0 {
				if errs != nil {
					t.Errorf("unexpected error: %s", errs)
				}

				return
			}

			if errs == nil {
				t.Errorf("expected error: %s", tc.errMsg)
				return
			}

			if got := errs.Error(); !strings.Contains(got, tc.errMsg) {
				t.Errorf("unexpected error: %s expected to contain %s", got, tc.errMsg)
			}
		})
	}
}
